﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Security.Cryptography;
using System.IO;
using System.Runtime.Serialization;
using System.Diagnostics;

namespace Pws.Clients.RestLibrary.Transport
{
	/*
	 * Copyright (C) PWS Distributors Ltd 2011-2013
	 * All rights reserved.
	 * 
	 */

	/// <summary>
	/// This is an example interface demonstrating a typical code library which a client might implement,
	/// in order to generate a PWS Rest Web Service call.
	/// This is to demonstrate the logic, and should not necessarily be used in production code.
	/// 
	/// Consider instantiating one object for the entire client application.
	/// 
	/// Instances and operations are thread safe.
	/// </summary>
	[System.Runtime.InteropServices.ClassInterface(System.Runtime.InteropServices.ClassInterfaceType.None)]
	[DebuggerDisplay("Service = {_deploymentUri}, ApiKey = {_apiKey}")]
	public class RequestFactory : IRequestFactory
	{
		private String _apiKey;
		private byte[] _privateKey;
		private readonly String _deploymentUri;

		/// <summary>
		/// This default constructor is only for use by COM.
		/// </summary>
		public RequestFactory() : this(Guid.Empty, Guid.Empty, DeploymentType.Production)
		{
		}

		/// <summary>
		/// Create an IRequestFactory configured for the Production version of PWS Rest Web Services.
		/// </summary>
		/// <param name="apiKey">The public API Key supplied to the customer.</param>
		/// <param name="privateKey">The private key supplied to the customer, used for signing messages.</param>
		/// <returns>An example interface for encapsulating PWS Rest Web Service calls.</returns>
		public RequestFactory(Guid apiKey, Guid privateKey) : this(apiKey, privateKey, DeploymentType.Production)
		{
		}

		/// <summary>
		/// Create an IRequestFactory configured for accessing a particular PWS Rest Web Services address.
		/// Consider using the overload which takes DeploymentType instead.
		/// </summary>
		/// <param name="apiKey">The public API Key supplied to the customer.</param>
		/// <param name="privateKey">The private key supplied to the customer, used for signing messages.</param>
		/// <param name="deploymentUri">Web Services deployment address, e.g http://services.pws.co.uk:8080/rest-staging </param>
		public RequestFactory(Guid apiKey, Guid privateKey, String deploymentUri)
		{
			_apiKey = apiKey.ToString();
			_privateKey = privateKey.ToByteArray();
			_deploymentUri = deploymentUri;
		}

		/// <summary>
		/// Create an IRequestFactory configured for a specific deployment version of PWS Rest Web Services.
		/// For example, this overload can be used for making calls to the Test service.
		/// </summary>
		/// <param name="apiKey">The public API Key supplied to the customer.</param>
		/// <param name="privateKey">The private key supplied to the customer, used for signing messages.</param>
		/// <param name="type">Deployment version to connect with.</param>
		public RequestFactory(Guid apiKey, Guid privateKey, DeploymentType type)
		{
			String deploymentUri = "http://services.pws.co.uk:8080";

			switch (type)
			{
				case DeploymentType.Production: deploymentUri += "/rest"; break;
				case DeploymentType.Staging: deploymentUri += "/rest-staging"; break;
				case DeploymentType.Dev: deploymentUri += "/rest-dev"; break;
				case DeploymentType.Localhost: deploymentUri = "http://localhost:57685"; break;
				case DeploymentType.LocalhostDebug: deploymentUri = "http://localhost.fiddler:57685"; break;
			}

			_apiKey = apiKey.ToString();
			_privateKey = privateKey.ToByteArray();
			_deploymentUri = deploymentUri;
		}

		/// <summary>
		/// For use only with COM. Sets the API key directly rather than through the constructor.
		/// </summary>
		/// <param name="apiKeyGuid">A string representation of a GUID.</param>
		public void SetApiKey(String apiKeyGuid)
		{
			_apiKey = new Guid(apiKeyGuid).ToString();
		}

		/// <summary>
		/// For use only with COM. Sets the private key directly rather than through the constructor.
		/// </summary>
		/// <param name="privateKeyGuid">A string representatino of a GUID.</param>
		public void SetPrivateKey(String privateKeyGuid)
		{
			_privateKey = new Guid(privateKeyGuid).ToByteArray();
		}

		/// <summary>
		/// The location of the main deployment, depending on the DeploymentType being used. The "Cool URI" location
		/// can be appended to this.
		/// </summary>
		public String DeploymentUri { get { return _deploymentUri; } }

		#region Public overloads for executing a REST method, used by the example Console Application.

		/// <summary>
		/// Perform a REST Web Service call to PWS, without any user authentication methods.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="responseMediaType">The response type to expect in return. E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="requestMediaType">The request type of the request message content. May be null, if no messageContent is being sent. E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="messageContent">Request message content.</param>
		/// <param name="sendHmac">True to specify that a HMAC should be calculated and sent in the request header.</param>
		/// <param name="useHttps">True to specify that HTTPS should be used.</param>
		/// <returns>WebResponse object. This must be Disposed once finished with.</returns>
		public WebResponse GetRestResponse(String requestMethod, Uri service, String requestMediaType, String responseMediaType, Stream messageContent)
		{
			WebRequest request = CreateWebRequest(requestMethod, service, requestMediaType, responseMediaType, messageContent);
			AssignHeaderHmac(request.Headers, requestMethod, request.RequestUri, messageContent);

			return request.GetResponse();
		}

		/// <summary>
		/// Perform a REST Web Service call to PWS, with token based user authentication. The HMAC is also included in the request, implicitly.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="responseMediaType">The response type to expect in return. E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="requestMediaType">The request type of the request message content. May be null, if no messageContent is being sent. E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="messageContent">Request message content.</param>
		/// <param name="token">A user authentication token which has been provided by the PWS Authentication Token Service.</param>
		/// <returns>WebResponse object. This must be Disposed once finished with.</returns>
		public WebResponse GetRestResponse(String requestMethod, Uri service, String requestMediaType, String responseMediaType, Stream messageContent, String token)
		{
			WebRequest request = CreateWebRequest(requestMethod, service, requestMediaType, responseMediaType, messageContent);
			String hmac = AssignHeaderHmac(request.Headers, requestMethod, request.RequestUri, messageContent);
			AssignHeaderAuthenticationToken(request.Headers, token, hmac);

			return request.GetResponse();
		}
		
		/// <summary>
		/// Perform a REST Web Service call to PWS, with token based user authentication. The HMAC is also included in the request, implicitly.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="responseMediaType">The response type to expect in return. E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="requestMediaType">The request type of the request message content. May be null, if no messageContent is being sent. E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="messageContent">Request message content.</param>
		/// <param name="username">The end user's username to use for authentication.</param>
		/// <param name="password">The end user's password to use for authentication.</param>
		/// <param name="sendHmac">True to specify that a HMAC should be calculated and sent in the request header.</param>
		/// <returns>WebResponse object. This must be Disposed once finished with.</returns>
		public WebResponse GetRestResponse(String requestMethod, Uri service, String requestMediaType, String responseMediaType, Stream messageContent, String username, String password)
		{
			WebRequest request = CreateWebRequest(requestMethod, service, requestMediaType, responseMediaType, messageContent);
			AssignHeaderHmac(request.Headers, requestMethod, request.RequestUri, messageContent);
			AssignHeaderBasicAuthentication(request.Headers, username, password);

			return request.GetResponse();
		}

		#endregion


		#region Private helpers which construct the HTTP request with the required X-PWS header values.

		/// <summary>
		/// Create a basic web request to the REST URI. This includes the mandatory X-PWS-ApiKey.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="messageContent">Request message content.</param>
		/// <returns>WebRequest</returns>
		private WebRequest CreateWebRequest(String requestMethod, Uri service, String requestMediaType, String responseMediaType, Stream messageContent)
		{
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(service);
			request.Method = requestMethod;
			request.Headers.Add("X-PWS-ApiKey", _apiKey);
			request.ContentType = requestMediaType ?? "application/xml";
			request.Accept = responseMediaType ?? "application/xml";
			request.Timeout = int.MaxValue;
			request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;

			if (requestMethod != WebRequestMethods.Http.Get && messageContent != null)
			{
				using (Stream stream = request.GetRequestStream())
				{
					messageContent.Seek(0, SeekOrigin.Begin);
					messageContent.CopyTo(stream);
				}
			}

			return request;
		}


		/// <summary>
		/// Calculate the HMAC components, and assign it to the appropriate X-PWS-Hmac (and related) header values.
		/// </summary>
		/// <param name="headers">The headers from the WebRequest, into which the HMAC will be assigned.</param>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="uri">The full URI of the service being called, e.g. http://services.pws.co.uk:8080/rest/example/v1/object/123?$filter=X eq y</param>
		/// <param name="timestamp">The ISO-8601 (or any unambiguous format) datetime of the request.</param>
		/// <param name="messageContent">The request message body content. This is allowed to be empty.</param>
		/// <returns>The X-PWS-Hmac content, for information purposes.</returns>
		private String AssignHeaderHmac(WebHeaderCollection headers, String requestMethod, Uri uri, Stream messageContent)
		{
			String timestamp = DateTime.Now.ToString("s");
			String uniqueId = Guid.NewGuid().ToString();

			using (Stream hmacSource = new MemoryStream())
			using (KeyedHashAlgorithm algorithm = new HMACSHA256(_privateKey))
			{
				byte[] bytesWithoutContent = ASCIIEncoding.UTF8.GetBytes(requestMethod + uri.AbsoluteUri + timestamp + uniqueId);
				hmacSource.Write(bytesWithoutContent, 0, bytesWithoutContent.Length);

				// If the message has some content (i.e. is not a GET, hence contains an object to be PUT/POSTed etc)
				// then append the message content to the hmacSource.
				if (messageContent != null)
				{
					messageContent.Seek(0, SeekOrigin.Begin);
					messageContent.CopyTo(hmacSource);
				}

				hmacSource.Seek(0, SeekOrigin.Begin);
				byte[] hmacResultAsBytes = algorithm.ComputeHash(hmacSource);
				String hmacResult = Convert.ToBase64String(hmacResultAsBytes);

				headers.Set("X-PWS-Hmac", hmacResult);
				headers.Set("X-PWS-Unique", uniqueId);
				headers.Set("X-PWS-Timestamp", timestamp);

				return hmacResult;
			}
		}


		/// <summary>
		/// Based upon the assumption that the HMAC has already been calculated,
		/// this calculates the X-PWS-UserToken and X-PWS-UserTokenHash header values and assigns them to the header.
		/// </summary>
		/// <param name="headers">The headers from the WebRequest, into which the UserTokens will be assigned.</param>
		/// <param name="token">Token string as passed.</param>
		/// <param name="hmac">The pre-calculated X-PWS-Hmac string.</param>
		private void AssignHeaderAuthenticationToken(WebHeaderCollection headers, String token, String hmac)
		{
			String hashSource = hmac + token;
			byte[] hashSourceAsBytes = ASCIIEncoding.UTF8.GetBytes(hashSource);

			using (KeyedHashAlgorithm algorithm = new HMACSHA256(_privateKey))
			{
				byte[] hashResultAsBytes = algorithm.ComputeHash(hashSourceAsBytes);
				String hashResult = Convert.ToBase64String(hashResultAsBytes);

				headers.Set("X-PWS-UserToken", token);
				headers.Set("X-PWS-UserTokenHash", hashResult);
			}
		}

		/// <summary>
		/// Assigns the Basic Authentication credentials to the standard header value.
		/// </summary>
		/// <param name="headers">The headers from the WebRequest, into which the Basic Authentication will be assigned.</param>
		/// <param name="username">The end user's username.</param>
		/// <param name="password">The end user's password.</param>
		private void AssignHeaderBasicAuthentication(WebHeaderCollection headers, String username, String password)
		{
			byte[] bytes = ASCIIEncoding.UTF8.GetBytes(username + ":" + password);
			headers.Set(HttpRequestHeader.Authorization, "Basic " + Convert.ToBase64String(bytes));
		}

		#endregion


		#region Wrapper methods

		/// <summary>
		/// Perform a REST Web Service call to PWS, with token based user authentication. The HMAC is also included in the request, implicitly.
		/// This will attempt to use the DataContractSerializer on the supplied and retrieved objects.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="mediaType">The response type to expect in return, and optionally for sending (if applicable). E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="sendObject">The object to send, if applicable. Must be null for a GET operation.</param>
		/// <param name="token">A user authentication token which has been provided by the PWS Authentication Token Service.</param>
		/// <param name="httpResponseHeaders">Returns the HTTP headers that were received with the response.</param>
		/// <returns>A deserialised object of type T. If this is not possible, then an exception is thrown.</returns>
		/// <typeparam name="T">The type of object being received (and optionally sent). This will be used for automatic serialisation to Xml.</typeparam>
		public T GetRestResponseAsObject<T>(string requestMethod, Uri service, string mediaType, T sendObject, string token, out WebHeaderCollection httpResponseHeaders) where T : class
		{
			// Create the DataContractSerializer for the object type being received (and possibly sent).
			DataContractSerializer dcs = new DataContractSerializer(typeof(T));

			// Create a memory stream for the send object's Xml.
			using (MemoryStream requestObjectStream = new MemoryStream())
			{
				// Only serialise the object if it is non-null, and the send method is not GET.
				if (requestMethod != WebRequestMethods.Http.Get && sendObject != null)
				{
					// Write send object's Xml using DCS.
					dcs.WriteObject(requestObjectStream, sendObject);
				}

				// Read the response (using the Core methods defined above) as a Stream.
				using (WebResponse response = GetRestResponse(requestMethod, service, mediaType, mediaType, requestObjectStream, token))
				using (Stream responseObjectStream = response.GetResponseStream())
				{
					httpResponseHeaders = response.Headers;

					// Use the DCS again to de-serialise the object from Xml into the request object.
					if (response.ContentLength != 0)
						return dcs.ReadObject(responseObjectStream) as T;
					else
						return default(T);
				}
			}
		}


		/// <summary>
		/// Perform a REST Web Service call to PWS, with token based user authentication. The HMAC is also included in the request, implicitly.
		/// This will attempt to use the DataContractSerializer on the retrieved object, with the supplied data being sent from a stream; useful for a Post of binary data.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="mediaTypeRequest">The request type to use for sending the binary data, e.g. image/png</param>
		/// <param name="mediaTypeResponse">The response type to expect in return.</param>
		/// <param name="sendData">The data to send. Must be null for a GET operation.</param>
		/// <param name="token">A user authentication token which has been provided by the PWS Authentication Token Service.</param>
		/// <param name="httpResponseHeaders">Returns the HTTP headers that were received with the response.</param>
		/// <returns>A deserialised object of type T. If this is not possible, then an exception is thrown.</returns>
		/// <exception cref="SerializationException">Thrown if the response did not contain an object of type T.</exception>
		/// <typeparam name="T">The type of object being received. This will be used for automatic deserialisation to Xml.</typeparam>
		[System.Runtime.InteropServices.ComVisible(false)]
		public T GetRestResponseAsObject<T>(string requestMethod, Uri service, string mediaTypeRequest, string mediaTypeResponse, Stream sendData, string token, out WebHeaderCollection httpResponseHeaders)
			where T : class
		{
			// Create the DataContractSerializer for the object type being sent, and another for the one being received.
			DataContractSerializer dcsResponse = new DataContractSerializer(typeof(T));

			// Read the response (using the Core methods defined above) as a Stream.
			using (WebResponse response = GetRestResponse(requestMethod, service, mediaTypeRequest, mediaTypeResponse, requestMethod != WebRequestMethods.Http.Get ? sendData : null, token))
			using (Stream responseObjectStream = response.GetResponseStream())
			{
				httpResponseHeaders = response.Headers;

				// Use the DCS to de-serialise the object from Xml into the request object.
				if (response.ContentLength != 0)
					return dcsResponse.ReadObject(responseObjectStream) as T;
				else
					return default(T);
			}
		}

		/// <summary>
		/// Perform a REST Web Service call to PWS, with token based user authentication. The HMAC is also included in the request, implicitly.
		/// This will attempt to use the DataContractSerializer on the supplied and retrieved objects.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="mediaType">The response type to expect in return, and optionally for sending (if applicable). E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="sendObject">The object to send, if applicable. Must be null for a GET operation.</param>
		/// <param name="token">A user authentication token which has been provided by the PWS Authentication Token Service.</param>
		/// <returns>A deserialised object of type T. If this is not possible, then an exception is thrown.</returns>
		/// <typeparam name="T">The type of object being received (and optionally sent). This will be used for automatic serialisation to Xml.</typeparam>
		public T GetRestResponseAsObject<T>(string requestMethod, Uri service, string mediaType, T sendObject, string token) where T : class
		{
			WebHeaderCollection headers = null;
			return GetRestResponseAsObject<T>(requestMethod, service, mediaType, sendObject, token, out headers);
		}

		/// <summary>
		/// Perform a REST Web Service call to PWS, with token based user authentication. The HMAC is also included in the request, implicitly.
		/// This will attempt to use the DataContractSerializer on the supplied and retrieved objects.
		/// </summary>
		/// <param name="requestMethod">HTTP request type, e.g. GET or POST.</param>
		/// <param name="service">The absolute URI to the service and object being called. </param>
		/// <param name="mediaType">The response type to expect in return, and optionally for sending (if applicable). E.g. application/x.pws.customers.orders.order-v1+xml</param>
		/// <param name="sendObject">The object to send, if applicable. Must be null for a GET operation.</param>
		/// <param name="username">The end user's username to use for authentication.</param>
		/// <param name="password">The end user's password to use for authentication.</param>
		/// <returns>A deserialised object of type T. If this is not possible, then an exception is thrown.</returns>
		/// <typeparam name="T">The type of object being received (and optionally sent). This will be used for automatic serialisation to Xml.</typeparam>
		public T GetRestResponseAsObject<T>(string requestMethod, Uri service, string mediaType, T sendObject, string username, string password) where T : class
		{
			// Create the DataContractSerializer for the object type being received (and possibly sent).
			DataContractSerializer dcs = new DataContractSerializer(typeof(T));

			// Create a memory stream for the send object's Xml.
			using (MemoryStream requestObjectStream = new MemoryStream())
			{
				// Write send object's Xml using DCS.
				dcs.WriteObject(requestObjectStream, sendObject);

				// Read the response (using the Core methods defined above) as a Stream.
				using (WebResponse response = GetRestResponse(requestMethod, service, mediaType, mediaType, requestObjectStream, username, password))
				using (Stream responseObjectStream = response.GetResponseStream())
				{
					// Use the DCS again to de-serialise the object from Xml into the request object.
					return dcs.ReadObject(responseObjectStream) as T;
				}
			}
		}

		#endregion

		
		#region COM specific and direct Xml methods

		public string GetRestResponseAsXml(string requestMethod, string service, string mediaType, string sendObject, string username, string password)
		{
			if (sendObject != null)
			{
				// Sending some Xml in the request, i.e. not a GET.
				using (MemoryStream stream = new MemoryStream())
				using (StreamWriter writer = new StreamWriter(stream))
				{
					writer.Write(sendObject);
					writer.Flush();
					stream.Seek(0, SeekOrigin.Begin);

					using (WebResponse response = GetRestResponse(requestMethod, new Uri(service), mediaType, mediaType, stream, username, password))
					{
						return GetXmlFromWebResponse(response);
					}
				}
			}
			else
			{
				// Not sending any Xml in the request.
				using (WebResponse response = GetRestResponse(requestMethod, new Uri(service), mediaType, mediaType, null, username, password))
				{
					return GetXmlFromWebResponse(response);
				}
			}
		}

		public string GetRestResponseAsXml(string requestMethod, string service, string mediaType, string sendObject, string token)
		{
			if (sendObject != null)
			{
				// Sending some Xml in the request, i.e. not a GET.
				using (MemoryStream stream = new MemoryStream())
				using (StreamWriter writer = new StreamWriter(stream))
				{
					writer.Write(sendObject);
					writer.Flush();
					stream.Seek(0, SeekOrigin.Begin);

					using (WebResponse response = GetRestResponse(requestMethod, new Uri(service), mediaType, mediaType, stream, token))
					{
						return GetXmlFromWebResponse(response);
					}
				}
			}
			else
			{
				// Not sending any Xml in the request.
				using (WebResponse response = GetRestResponse(requestMethod, new Uri(service), mediaType, mediaType, null, token))
				{
					return GetXmlFromWebResponse(response);
				}
			}
		}

		/// <summary>
		/// Interpret the web response as Xml.
		/// </summary>
		/// <param name="response"></param>
		/// <returns>Xml string, if valid. Empty string if no result, or if the Xml was invalid.</returns>
		private string GetXmlFromWebResponse(WebResponse response)
		{
			try
			{
				using (Stream stream = response.GetResponseStream())
				using (System.Xml.XmlReader xmlReader = System.Xml.XmlReader.Create(stream))
				{
					System.Xml.Linq.XDocument xdoc = System.Xml.Linq.XDocument.Load(xmlReader);
					return xdoc.ToString(System.Xml.Linq.SaveOptions.DisableFormatting);
				}
			}
			catch (Exception ex)
			{
				return String.Empty;
			}
		}
		#endregion
	}

	#region Extension methods
	/// <summary>
	/// To retain .Net 3.5 support, avoid using Stream.CopyTo by providing an extension method for Stream.
	/// </summary>
	public static class StreamExtensionMethods
	{
		/// <summary>
		/// Copy data from the source stream to the destination stream, starting from the current positions.
		/// </summary>
		/// <param name="source">Stream to copy from</param>
		/// <param name="destination">Stream to copy to</param>
		public static void CopyTo(this Stream source, Stream destination)
		{
			int count;
			byte[] buffer = new byte[65536];
			while ((count = source.Read(buffer, 0, buffer.Length)) != 0)
			{
				destination.Write(buffer, 0, count);
			}
		}
	}
	#endregion
}
